package edu.ssdut.revise.core.refactoringDetection;

import com.github.javaparser.ast.body.*;
import com.github.javaparser.utils.Pair;
import edu.ssdut.revise.core.match.TwoFileMatcher;
import edu.ssdut.revise.models.NodePairWithRefactorings;
import edu.ssdut.revise.models.RefactoringType;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Detects and stores the detected refactorings between two corresponding CUs
 */
public class TwoFileRefactoringDetector {
    TwoFileMatcher matcher; // matched and unmatched elements between two files, no need to be serialized
    // TODO：这样实现的问题：需要每个类型一个变量，且调用很麻烦，需要先CUMatcher，然后TFMatcher，最后再RefactoringDetector
    List<NodePairWithRefactorings<MethodDeclaration>> methodRefactorings = new ArrayList<>();
    List<NodePairWithRefactorings<ConstructorDeclaration>> constructorRefactorings = new ArrayList<>();
    List<NodePairWithRefactorings<TypeDeclaration<?>>> typeDeclarationRefactorings = new ArrayList<>();
    List<NodePairWithRefactorings<VariableDeclarator>> variableRefactorings = new ArrayList<>();

    /**
     * constructor with the given matcher
     *
     * @param matcher the TwoFileMatcher that has done matching.( .match() function has been called)
     */
    public TwoFileRefactoringDetector(TwoFileMatcher matcher) {
        this.matcher = matcher;
    }

    /**
     * detect refactorings between certain elements
     */
    public void detect() {
        // field refactoring detect
        fieldRefactoringDetect();

        // method refactorings detect
        methodRefactoringDetect();

        // constructor refactorings detect
        constructorRefactoringDetect();

        //TODO: classOrInterface/enum refactoring detect
        topLevelElementRefactoringDetect();

    }

    /**
     * detect top level element refactorings
     */
    private void topLevelElementRefactoringDetect() {
        // matched
        List<Pair<TypeDeclaration<?>, TypeDeclaration<?>>> matchedTypes = matcher.getTypeDeclarationMatchStore().getMatched();
        for (Pair<TypeDeclaration<?>, TypeDeclaration<?>> pair : matchedTypes) {
            TypeDeclaration<?> a = pair.a;
            TypeDeclaration<?> b = pair.b;

            NodePairWithRefactorings<TypeDeclaration<?>> tdRefactoringsForOneTypePair = new NodePairWithRefactorings<>(List.of(a), List.of(b));
            if (!a.getModifiers().containsAll(b.getModifiers()) || !b.getModifiers().containsAll(a.getModifiers())) {
                tdRefactoringsForOneTypePair.getRefactoringTypes().add(RefactoringType.TYPE_MODIFIERS_CHANGED);
            }

            // if refactoring detected
            if (tdRefactoringsForOneTypePair.getRefactoringTypes().size() > 0) {
                typeDeclarationRefactorings.add(tdRefactoringsForOneTypePair);
            }
        }

        // unmatched
        List<TypeDeclaration<?>> unmatchedNodes1 = matcher.getTypeDeclarationMatchStore().getUnmatchedNodes1();
        List<TypeDeclaration<?>> unmatchedNodes2 = matcher.getTypeDeclarationMatchStore().getUnmatchedNodes2();
        for (TypeDeclaration<?> td : unmatchedNodes1) {
            NodePairWithRefactorings<TypeDeclaration<?>> tdRefactoringsForOneType = new NodePairWithRefactorings<>();
            // all marked as deleted
            tdRefactoringsForOneType.getRefactoringTypes().add(RefactoringType.TYPE_DELETED);
            tdRefactoringsForOneType.addNode1(td);
            typeDeclarationRefactorings.add(tdRefactoringsForOneType);
        }
        for (TypeDeclaration<?> td : unmatchedNodes2) {
            NodePairWithRefactorings<TypeDeclaration<?>> tdRefactoringsForOneType = new NodePairWithRefactorings<>();
            // all marked as added
            tdRefactoringsForOneType.getRefactoringTypes().add(RefactoringType.TYPE_ADDED);
            tdRefactoringsForOneType.addNode2(td);
            typeDeclarationRefactorings.add(tdRefactoringsForOneType);
        }
    }

    /**
     * detect method refactorings
     */
    private void methodRefactoringDetect() {
        // matched
        List<Pair<MethodDeclaration, MethodDeclaration>> matchedMethods = matcher.getMethodDeclarationMatchStore().getMatched();
        for (Pair<MethodDeclaration, MethodDeclaration> pair : matchedMethods) {
            MethodDeclaration a = pair.a;
            MethodDeclaration b = pair.b;

            NodePairWithRefactorings<MethodDeclaration> mdRefactoringsForOneMethodPair = new NodePairWithRefactorings<>(List.of(a), List.of(b));
            if (!a.getNameAsString().equals(b.getNameAsString())) {
                mdRefactoringsForOneMethodPair.getRefactoringTypes().add(RefactoringType.METHOD_RENAMED);
            }
            if (!a.getModifiers().containsAll(b.getModifiers()) || !b.getModifiers().containsAll(a.getModifiers())) {
                mdRefactoringsForOneMethodPair.getRefactoringTypes().add(RefactoringType.METHOD_MODIFIERS_CHANGED);
            }
            if (a.getBody().isPresent() && b.getBody().isPresent()) {
                String bodyA = a.getBody().get().toString();
                String bodyB = b.getBody().get().toString();
                if (!Objects.equals(bodyA, bodyB)) {
                    // method changed
                    mdRefactoringsForOneMethodPair.getRefactoringTypes().add(RefactoringType.METHOD_CHANGED);
                }
            } else if (a.getBody().isPresent() ^ b.getBody().isPresent()) {
                // in what situation will the body of a method not be present? interface?
                // method changed
                mdRefactoringsForOneMethodPair.getRefactoringTypes().add(RefactoringType.METHOD_CHANGED);
            }
            // if refactoring detected
            if (mdRefactoringsForOneMethodPair.getRefactoringTypes().size() > 0) {
                methodRefactorings.add(mdRefactoringsForOneMethodPair);
            }
        }

        // unmatched
        List<MethodDeclaration> unmatchedNodes1 = matcher.getMethodDeclarationMatchStore().getUnmatchedNodes1();
        List<MethodDeclaration> unmatchedNodes2 = matcher.getMethodDeclarationMatchStore().getUnmatchedNodes2();
        for (MethodDeclaration md : unmatchedNodes1) {
            NodePairWithRefactorings<MethodDeclaration> mdRefactoringsForOneMethod = new NodePairWithRefactorings<>();
            // all marked as deleted
            mdRefactoringsForOneMethod.getRefactoringTypes().add(RefactoringType.METHOD_DELETED);
            mdRefactoringsForOneMethod.addNode1(md);
            methodRefactorings.add(mdRefactoringsForOneMethod);
        }
        for (MethodDeclaration md : unmatchedNodes2) {
            NodePairWithRefactorings<MethodDeclaration> mdRefactoringsForOneMethod = new NodePairWithRefactorings<>();
            // all marked as added
            mdRefactoringsForOneMethod.getRefactoringTypes().add(RefactoringType.METHOD_ADDED);
            mdRefactoringsForOneMethod.addNode2(md);
            methodRefactorings.add(mdRefactoringsForOneMethod);
        }

    }

    /**
     * detect method refactorings
     */
    private void constructorRefactoringDetect() {
        // matched
        List<Pair<ConstructorDeclaration, ConstructorDeclaration>> matchedConstructors = matcher.getConstructorDeclarationMatchStore().getMatched();
        for (Pair<ConstructorDeclaration, ConstructorDeclaration> pair : matchedConstructors) {
            ConstructorDeclaration a = pair.a;
            ConstructorDeclaration b = pair.b;

            NodePairWithRefactorings<ConstructorDeclaration> cdRefactoringsForOneMethodPair = new NodePairWithRefactorings<>(List.of(a), List.of(b));
            // TODO: to detect rename need to first add bottom-up matching

            String bodyA = a.getBody().toString();
            String bodyB = b.getBody().toString();
            if (!Objects.equals(bodyA, bodyB)) {
                // constructor changed
                cdRefactoringsForOneMethodPair.getRefactoringTypes().add(RefactoringType.CONSTRUCTOR_CHANGED);
            }

            // if refactoring detected
            if (cdRefactoringsForOneMethodPair.getRefactoringTypes().size() > 0) {
                constructorRefactorings.add(cdRefactoringsForOneMethodPair);
            }
        }

        // unmatched
        List<ConstructorDeclaration> unmatchedNodes1 = matcher.getConstructorDeclarationMatchStore().getUnmatchedNodes1();
        List<ConstructorDeclaration> unmatchedNodes2 = matcher.getConstructorDeclarationMatchStore().getUnmatchedNodes2();
        for (ConstructorDeclaration cd : unmatchedNodes1) {
            NodePairWithRefactorings<ConstructorDeclaration> cdRefactoringsForOneMethod = new NodePairWithRefactorings<>();
            // all marked as deleted
            cdRefactoringsForOneMethod.getRefactoringTypes().add(RefactoringType.CONSTRUCTOR_DELETED);
            cdRefactoringsForOneMethod.addNode1(cd);
            constructorRefactorings.add(cdRefactoringsForOneMethod);
        }
        for (ConstructorDeclaration cd : unmatchedNodes2) {
            NodePairWithRefactorings<ConstructorDeclaration> cdRefactoringsForOneMethod = new NodePairWithRefactorings<>();
            // all marked as added
            cdRefactoringsForOneMethod.getRefactoringTypes().add(RefactoringType.CONSTRUCTOR_ADDED);
            cdRefactoringsForOneMethod.addNode2(cd);
            constructorRefactorings.add(cdRefactoringsForOneMethod);
        }

    }

    /**
     * detect field refactorings
     */
    private void fieldRefactoringDetect() {
        // matched
        List<Pair<VariableDeclarator, VariableDeclarator>> matchedVariables = matcher.getVariableDeclaratorMatchStore().getMatched();
        //TODO: MOVED
        for (Pair<VariableDeclarator, VariableDeclarator> pair : matchedVariables) {
            VariableDeclarator a = pair.a;
            VariableDeclarator b = pair.b;
            NodePairWithRefactorings<VariableDeclarator> vdRefactoringsForOneVariable = new NodePairWithRefactorings<>(List.of(a), List.of(b));
            if (!a.getNameAsString().equals(b.getNameAsString())) {
                vdRefactoringsForOneVariable.getRefactoringTypes().add(RefactoringType.FIELD_RENAMED);
            }
            if (a.getParentNode().isPresent() && b.getParentNode().isPresent()) {
                if (a.getParentNode().get() instanceof FieldDeclaration && b.getParentNode().get() instanceof FieldDeclaration) {
                    FieldDeclaration fieldA = (FieldDeclaration) a.getParentNode().get();
                    FieldDeclaration fieldB = (FieldDeclaration) b.getParentNode().get();
                    if (!fieldA.getModifiers().containsAll(fieldB.getModifiers()) || !fieldB.getModifiers().containsAll(fieldA.getModifiers())) {
                        vdRefactoringsForOneVariable.getRefactoringTypes().add(RefactoringType.FIELD_MODIFIERS_CHANGED);
                    }
                }
            }

            if (vdRefactoringsForOneVariable.getRefactoringTypes().size() > 0) {
                variableRefactorings.add(vdRefactoringsForOneVariable);
            }
        }


        // unmatched
        List<VariableDeclarator> unmatchedNodes1 = matcher.getVariableDeclaratorMatchStore().getUnmatchedNodes1();
        List<VariableDeclarator> unmatchedNodes2 = matcher.getVariableDeclaratorMatchStore().getUnmatchedNodes2();
        for (VariableDeclarator vd : unmatchedNodes1) {
            NodePairWithRefactorings<VariableDeclarator> vdRefactoringsForOneVariable = new NodePairWithRefactorings<>();
            // all marked as deleted
            vdRefactoringsForOneVariable.getRefactoringTypes().add(RefactoringType.FIELD_DELETED);
            vdRefactoringsForOneVariable.addNode1(vd);
            variableRefactorings.add(vdRefactoringsForOneVariable);
        }
        for (VariableDeclarator vd : unmatchedNodes2) {
            NodePairWithRefactorings<VariableDeclarator> vdRefactoringsForOneField = new NodePairWithRefactorings<>();
            // all marked as added
            vdRefactoringsForOneField.getRefactoringTypes().add(RefactoringType.FIELD_ADDED);
            vdRefactoringsForOneField.addNode2(vd);
            variableRefactorings.add(vdRefactoringsForOneField);
        }

    }

    /**
     * detect class or interface refactorings
     */
    private void classOrInterfaceRefactoringDetect() { // TODO
        // matched
    }

    public List<NodePairWithRefactorings<MethodDeclaration>> getMethodRefactorings() {
        return methodRefactorings;
    }

    public List<NodePairWithRefactorings<TypeDeclaration<?>>> getTypeDeclarationRefactorings() {
        return typeDeclarationRefactorings;
    }

    public List<NodePairWithRefactorings<VariableDeclarator>> getVariableRefactorings() {
        return variableRefactorings;
    }

    public List<NodePairWithRefactorings<ConstructorDeclaration>> getConstructorRefactorings() {
        return constructorRefactorings;
    }
}
